(function() {
	var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
	window.requestAnimationFrame = requestAnimationFrame;
})();

function _(id)
{
	return document.getElementById(id);
}

var GUI = {
	className: 'GUI',

	frameDuration: 1000/60,	// This will be automatically set later
	fps: 60,	// ...and this too

	imagesPath: './data/images/',

	storage: null,
	engine: null,
	hud: null,
	camera: null,
	windows: [],

	width: 800,
	height: 480,

	mouseX: 0,
	mouseY: 0,

	Canvas: function(id)
	{
		this.canvas = _(id);
		this.context = this.canvas.getContext('2d');

		this.patterns = [];

		this.get2d = function()
		{
			return this.context;
		}

		this.createPattern = function(url, repeat)
		{
			if(!repeat)
			{
				repeat = 'repeat';
			}
			if(!this.patterns[url+':'+repeat])
			{
				var img = new Image();
				img.src = url;
				this.patterns[url+':'+repeat] = this.context.createPattern(img, repeat);
			}
			return this.patterns[url+':'+repeat];
		}

		this.fillRect = function(fillStyle, x, y, w, h, fromCenter, strokeStyle, lineWidth)
		{
			if(fromCenter)
			{
				x -= w/2;
				y -= h/2;
			}
			this.context.save();
			this.context.fillStyle = fillStyle;

			this.context.fillRect(x, y, w, h);

			if(strokeStyle && lineWidth)
			{
				this.context.strokeStyle = strokeStyle;
				this.context.lineWidth = lineWidth;
				this.context.strokeRect(x, y, w, h);
			}

			this.context.restore();
		}

		this.drawImage = function(img, sx, sy, sw, sh, dx, dy)
		{
			this.context.drawImage(img, sx, sy, sw, sh, dx, dy, sw, sh);
		}

		this.fillText = function(fillStyle, /*strokeStyle, strokeWidth,*/ x, y, font, text, fromCenter)
		{
/*			if(fromCenter)
			{
				x -= w/2;
				y -= h/2;
			}*/
			this.context.save();
			this.context.fillStyle = fillStyle,
//			this.context.strokeStyle = strokeStyle,
//			this.context.strokeWidth = strokeWidth,
			this.context.font = font;
			if(fromCenter)
			{
				this.context.textBaseLine = "middle";
				this.context.textAlign = "center";
			}
//			console.log('s: '+text+', length: '+text.length);
			this.context.fillText(text, x, y);
			this.context.restore();
		}

		this.fillArc = function(fillStyle, x, y, r, start, end)
		{
			if(!start) start = 0;
			if(!end) end = 2*Math.PI;
			this.context.save();
			this.context.fillStyle = fillStyle;
			this.context.beginPath();
			this.context.moveTo(x, y);
			this.context.arc(x, y, r, start, end);
			this.context.lineTo(x, y);
			this.context.closePath();
			this.context.fill();
			this.context.restore();
		}

		this.fillSquareArc = function(fillStyle, x, y, r, start, end)
		{
			this.context.save();
			this.context.beginPath();
			this.context.rect(x-r, y-r, r*2, r*2);
			this.context.clip();
			this.fillArc(fillStyle, x, y, r*1.5, start, end);
			this.context.restore();
		}

		this.translateCanvas = function(x, y)
		{
			this.context.translate(x, y);
		}

		this.width = function()
		{
			return this.canvas.width;
		}

		this.height = function()
		{
			return this.canvas.height;
		}

		this.keydown = function(cb)
		{
			this.canvas.onkeydown = cb;
		}

/*		this.keyup = function(cb)
		{
			this.canvas.onkeyup = cb;
		}*/

		this.mousedown = function(cb)
		{
			this.canvas.onmousedown = cb;
		}

		this.mouseup = function(cb)
		{
			this.canvas.onmouseup = cb;
		}

		this.mousemove = function(cb)
		{
			this.canvas.onmousemove = cb;
		}

		this.contextmenu = function(cb)
		{
			this.canvas.oncontextmenu = cb;
		}
	},

	Engine: function(id)
	{

		GUI.StrObject.path = './data/objects/';
		GUI.StrObject.metaType = 'object';
		GUI.StrObject.IDLE = 0;
		GUI.StrObject.MOVING = 1;

		GUI.StrMap.path = './data/maps/';
		GUI.StrMap.metaType = 'map';

		GUI.Window.HAS_NONE = 0
		GUI.Window.HAS_TITLEBAR = 1
		GUI.Window.HAS_BODY = 2
		GUI.Window.HAS_CLOSE = 3

		this.canvas = new GUI.Canvas(id);
		this.storage = new GUI.Storage();

		GUI.storage = this.storage;
		GUI.engine = this;

		this.cursor = null;

		this.date = new Date();
		this.dev = _('dev');

		this.isDrag = false;

		var obj = this;

		this.objects = [];
		this.player = null;

		this.camera = new GUI.Camera(this.canvas);
		GUI.camera = this.camera;

//		this.particles = new GUI.Particles(350, 270, 30, 2, '#000', this.camera);
//		this.shakeEffect = new GUI.ShakeEffect(3);

		this.loadingScreen = new GUI.LoadingScreen();

//		this.testWindow = new GUI.Window('New test window');
//		this.testWindow.setGeometry(200, 100, 250, 200);

		this.dragging = null;

		this.loading = true;

		this.map = null;

		this.onAuthFail = function(){alert('PLEASE OVERRIDE `onAuthFail` FOR GOD\'S SAKE')}

		this.initCanvas = function()
		{
			this.canvas.fillRect('#111', 0, 0, this.canvas.width(), this.canvas.height(), false);
/*			this.canvas.drawLine({
				strokeStyle: '#000',
				strokeWidth: 1,
				x1: 0, y1: 0,
				x2: 800, y2: 480
			});*/

//			new (GUI.LoadingScreen()).draw(this.canvas);

			var obj = this;

			this.loadingScreen.draw(this.canvas);

			this.canvas.keydown(function(evt){
				obj.keyPress(evt.keyCode);
//				evt.preventDefault();
			});

			this.canvas.mousedown(function(evt){
				obj.mouseDown(evt.clientX - this.offsetLeft, evt.clientY - this.offsetTop, evt.button);
			});

			this.canvas.contextmenu(function(evt){
				return false;
			});

			this.canvas.mouseup(function(evt){
				obj.mouseUp(evt.clientX - this.offsetLeft, evt.clientY - this.offsetTop);
			});

			this.canvas.mousemove(function(evt){
				obj.mouseMove(evt.clientX - this.offsetLeft, evt.clientY - this.offsetTop);
			});

			this.storage.precacheObjects({objects:['girltest','girl1','tree1','tornado'],maps:['test'],images:['speech-link.png','hud.png','cursor-sword.png','flag.png','skills/tornado.png']}, function(count){
				console.log('Done precaching '+count+' objects');

				obj.cursor = obj.storage.getImage('cursor-sword.png');

				GUI.hud = new GUI.HUD();

//				alert(obj.cursor);

//				var girltest = obj.storage.getObject('girltest');
//				girltest.setAnimation('walk', 800);
//				obj.objects.push(girltest);

/*				var i = 1;
				window.setInterval(function(){
					girl1.setAnimation('walk-'+(i++), 600);
					if(i == 4)
					{
						i = 0;
					}
				}, 2000);*/

/*				window.setTimeout(function(){
					obj.loading = false;
					obj.startPainter();
				}, 1000);*/
			});
		}

		this.framesStart = (new Date()).getTime();
		this.lastRepaint = (new Date()).getTime();
		this.frames = 0;

		this.loadMap = function()
		{
//			this.map = new GUI.StrMap('test');
		}

		this.startPainter = function()
		{
			var obj = this;
			var repaint = function()
			{
				obj.repaint((new Date()).getTime());
//				window.setTimeout(repaint, 16);
//				requestAnimationFrame(repaint);
			}
//			repaint();
			window.setInterval(repaint, 25);
		}

		this.repaint = function(time)
		{
			//now = (new Date()).getTime();
//			var context = _('canvas').getContext('2d');

//			context.fillRect(0, 0, 800, 480);

			if(/*this.loading || */this.storage.isLoading() || !this.map || !this.player)
			{
				this.loadingScreen.draw(this.canvas);
			}
			else
			{
				this.camera.updatePos();

				this.canvas.fillRect('#111', 0, 0, this.canvas.width(), this.canvas.height(), false);

				this.player.setLookDir(this.player.getDir(GUI.mouseX - GUI.width/2, GUI.mouseY - GUI.height/2));

				// Does canvas translation. Careful!
				this.map.draw(this.canvas, this.camera);

				// Sort objects
				this.objects.sort(function(a,b){
					return (a.y - b.y);
				});

				for(key in this.objects)
				{
					this.objects[key].draw(this.canvas, this.camera);
				}

//				this.testWindow.draw(this.canvas);
				for(key in GUI.windows)
				{
					if(GUI.windows[key].alive)
					{
						GUI.windows[key].draw(this.canvas);
					}
					else
					{
						GUI.windows.splice(key, 1);
					}
				}

				// Finally, draw HUD
				GUI.hud.draw(this.canvas);

				this.canvas.drawImage(this.cursor, 0, 0, 32, 32, GUI.mouseX, GUI.mouseY);

//				this.particles.draw(this.canvas, this.camera);

//				this.shakeEffect.draw(this.canvas);
			}

//			console.log(time - this.lastRepaint);
			this.lastRepaint = time;
			this.frames++;
		}

		window.setInterval(function(){
			var framesEnd = (new Date()).getTime();
			GUI.fps = ~~(obj.frames / (framesEnd - obj.framesStart) * 1000);
			GUI.frameDuration = 1000 / ((GUI.fps < 20)?GUI.fps:60);
//			console.log('FPS is '+obj.fps);
			obj.dev.innerHTML = 'FPS: '+GUI.fps;
			obj.frames = 0;
			obj.framesStart = (new Date()).getTime();
		}, 500);

		this.keyPress = function(code)
		{
			console.log('Key pressed: '+code);

			// Skill used - 1, 2, 3, ..., 0
			if((code >= 48) && (code <= 57))
			{
				var skill = (code == 48 ? 58 : code) - 49;
				if(GUI.hud._skillReady(skill))
				{
					this.useSkill(skill);
				}
				else
				{
					console.log('Skill not yet ready!');
				}
			}
			else if(code == 86)
			{
				var window = new GUI.Window('New test window');
				window.setGeometry(200, 100, 250, 200);
				GUI.windows.push(window);
			}
		}

		this.mouseDown = function(x, y, button)
		{
			this.isDrag = true;
//			console.log('mouseDown('+x+', '+y+', '+button+')');
			var where;
			var cursorCaught = false;
			for(key in GUI.windows)
			{
				if(where = GUI.windows[key].testCursor(x, y))
				{
					console.log(where);
					cursorCaught = true;
					// Title clicked
					if(where == GUI.Window.HAS_TITLEBAR)
					{
						this.dragging = GUI.windows[key];
					}
					// Close clicked
					else if(where == GUI.Window.HAS_CLOSE)
					{
						GUI.windows[key].close();
					}
					// Body clicked
					else
					{
					}
				}
			}

			if(!cursorCaught)
			{
				if(this.player)
				{
					var realX = ~~(this.camera.getX() + x);
					var realY = ~~(this.camera.getY() + y);
					this.player.moveTo(realX, realY, true);
					client.send({a:'moveto',to:{x:realX,y:realY}});
				}
			}
		}

		this.mouseUp = function(x, y)
		{
			if(this.dragging)
			{
				this.dragging = null;
			}
			this.isDrag = false;
//			console.log('mouseUp('+x+', '+y+')');
		}

		this.mouseMove = function(x, y)
		{
			var dx = x - GUI.mouseX;
			var dy = y - GUI.mouseY;

			if(this.isDrag && this.dragging)
			{
//				console.log(this.testWindow.testCursor(x, y));
//				if(this.dragging.testCursor(x, y) == GUI.Window.HAS_TITLEBAR)
//				{
//					this.testWindow.drag(dx, dy);
//				}
				this.dragging.drag(dx, dy);
			}

			GUI.mouseX = x;
			GUI.mouseY = y;
//			console.log('mouseMove('+x+', '+y+')'+(this.isDrag?' (DRAG)':''));
		}

		this.setPlayerPos = function(pos)
		{
			this.player.setPos(pos.x, pos.y);
		}

		this.setPlayerNodeId = function(id)
		{
			this.player.setNodeId(id);
		}

		this.addPlayer = function(data, self)
		{
//			obj.storage.precacheObject('girl1');
			var player = obj.storage.getObject(data.object);
			player.setPos(data.at.x, data.at.y);
			player.setNodeId(data.nodeId);
			console.log('New player with nodeId '+data.nodeId)
			player.updateTexture(data.texture);
			player.setTitle(data.name);
			console.log('Player MS: '+data.ms);
			player.setMoveSpeed(data.ms);

			obj.objects.push(player);
			if(self)
			{
				player.setTitle('< YOU >');
				this.player = player;
				this.map = this.storage.getMap(data.map);
				this.camera.attachTo(player);
			}
		}

		this.addObject = function(object)
		{
			this.objects.push(object);
		}

		this.dispatchPacket = function(packet)
		{
			if(packet.a == 'message')
			{
				for(key in this.objects)
				{
					if(this.objects[key].nodeId == packet.nodeId)
					{
						this.objects[key].attachSpeech(/*packet.sender+': '+*/packet.message, 5000);
					}
				}
			}
			else if(packet.a == 'in')
			{
				if(packet.type == 'player')
				{
					console.log('New player!');
					this.addPlayer(packet)
				}
				else
				{
					console.log('New object.');
					var object = this.storage.getObject(packet.object);
					if(packet.nodeId) object.setNodeId(packet.nodeId);
					if(packet.at) object.setPos(packet.at.x, packet.at.y);
					if(packet.texture) object.updateTexture(packet.texture);
					if(packet.name) object.setTitle(packet.name);
					if(packet.ms) object.setMoveSpeed(packet.ms);

					this.addObject(object);
				}
			}
			else if(packet.a == 'out')
			{
				for(key in this.objects)
				{
					if(this.objects[key].nodeId == packet.nodeId)
					{
						this.objects.splice(key, 1);
					}
				}
			}
			else if(packet.a == 'moveto')
			{
				for(key in this.objects)
				{
					if(this.objects[key].nodeId == packet.nodeId)
					{
//						this.objects[key].setPos(packet.from.x, packet.from.y);
						this.objects[key].moveTo(packet.to.x, packet.to.y);
					}
				}
			}
			else if(packet.a == 'auth')
			{
				if(packet.success)
				{
/*					var player = obj.storage.getObject('girl1');
					player.setPos(400, 240);
					player.setTitle('< YOU >');

					this.camera.attachTo(player);

					player.onMove = function(dir)
					{
						this.setAnimation('walk-'+dir, 600);
					}
					player.onIdle = function()
					{
						this.setAnimation('default', 600);
					}

					this.player = player;
					obj.objects.push(player);*/

					this.loadMap(packet.map);

					this.addPlayer(packet, true)

//					this.setPlayerNodeId(packet.nodeId);
//					this.setPlayerPos(packet.at);
				}
				else
				{
//					Cookie.delete('login');
//					Cookie.delete('password');
					this.onAuthFail();
//					alert('Auth failed!');
				}
			}
			else if(packet.a == 'away')
			{
				for(key in this.objects)
				{
					if(this.objects[key].nodeId == packet.nodeId)
					{
						this.objects[key].setSubTitle('(away)', '#F00')
					}
				}
			}
			else if(packet.a == 'online')
			{
				for(key in this.objects)
				{
					if(this.objects[key].nodeId == packet.nodeId)
					{
						this.objects[key].setSubTitle('', '#0F0')
					}
				}
			}
			else if(packet.a == 'cooldown')
			{
				GUI.hud.startCooldown(packet.skill, packet.cooldown);
			}
			else
			{
				console.log('!!! Need to dispatch packet '+JSON.stringify(packet));
			}
		}

		this.useSkill = function(skill)
		{
			console.log('Using skill '+skill);
			client.send({'a':'use-skill','skill':skill,'to':{'x':GUI.camera.getX() + GUI.mouseX,'y':GUI.camera.getY() + GUI.mouseY}})
		}

		this.initCanvas();
		this.startPainter();
	},

	Storage: function()
	{
		this.objects = {};
		this.images = {};
//		this.path = './data/';

		var obj = this;

		this.lastCacheToDo = 0;
		this.cacheToDo = 0;

		this._onReady = function(count){alert(count+' objects precached');}

		this.isLoading = function()
		{
			return this.cacheToDo > 0;
		}

		this.precacheObjects = function(names, onReady){
			console.log('Starting precache...');
			total = 0;
			for(key in names)
			{
				for(key2 in names[key])
				{
					total ++;
				}
			}
			this.cacheToDo += total;
			this.lastCacheToDo = total;
			if(onReady)
			{
				this._onReady = onReady;
			}
			for(key in names.objects)
			{
				console.log('Precaching object '+names.objects[key]);
				this.precacheObject(names.objects[key], GUI.StrObject);
			}
			for(key in names.maps)
			{
				console.log('Precaching map '+names.maps[key]);
				this.precacheObject(names.maps[key], GUI.StrMap);
			}
			for(key in names.images)
			{
				console.log('Precaching image '+names.images[key]);
				this.precacheImage(GUI.imagesPath+names.images[key]);
			}
		}

		this.precacheObject = function(name, classProto)
		{
			// If already precached...
			if(obj.objects[classProto.metaType] && obj.objects[classProto.metaType][name])
			{
				console.log('Object '+name+' of type '+classProto.metaType+' already precached!');
				obj.cacheToDo--;
				obj.checkComplete();
			}
			// Else - precache
			else
			{
				$.ajax({
					url: classProto.path + name + '/meta.obj',
					dataType: 'json',
					success: function(data){
/*						s = '';
						for(i in data)
						{
							s += i+': '+data[i]+'\n';
						}
						alert(s);*/
						obj.cacheToDo--;
						console.log('Loaded object '+name);
						if(!obj.objects[classProto.metaType])
						{
							obj.objects[classProto.metaType] = {};
						}
						obj.objects[classProto.metaType][name] = new classProto(name, data);
						obj.checkComplete();
					},
					error: function(){
						console.log('CRITICAL: Failed to precache object `'+name+'`');
					}
				});
			}
		}

		this.precacheImage = function(url)
		{
			// If already precached...
			if(obj.images[url])
			{
				obj.cacheToDo--;
				obj.checkComplete();
			}
			// Else - precache
			else
			{
				var img = new Image();
				img.onload = function(){
					obj.cacheToDo--;
					obj.images[url] = this;
					obj.checkComplete();
				};
				img.src = url;
			}
		}

		this.checkComplete = function()
		{
			if(!obj.cacheToDo)
			{
				obj._onReady(obj.lastCacheToDo);
			}
		}

		this.getObject = function(name)
		{
			if(!this.objects[GUI.StrObject.metaType][name])
			{
				console.log('Object `'+name+'` is not loaded!');
			}
			// Return new instance
			return $.extend({}, this.objects[GUI.StrObject.metaType][name]);
		}

		this.getMap = function(name)
		{
			if(!this.objects[GUI.StrMap.metaType][name])
			{
				console.log('Map `'+name+'` is not loaded!');
			}
			// Return new instance
			return $.extend({}, this.objects[GUI.StrMap.metaType][name]);
		}

		this.getImage = function(name)
		{
			if(!this.images[GUI.imagesPath + name])
			{
				console.log('Image `'+name+'` is not loaded!');
			}
			return this.images[GUI.imagesPath + name];
		}
	},

	StrObject: function(name, data)
	{
//		GUI.StrObject.metaType = 'StrObject';

		this.name = name;
		this.data = data;
		this.animation = data.animations.default;
		this.currentFrame = 0;
		this.frameDuration = 100;
		this.currentTime = 0;

		this.nodeId = null;

		this.x = 0;
		this.y = 0;

		this.tx = 0;
		this.ty = 0;

		this.ms = 0;

		this.dir = 0;

		this.moveStarted = null;
		this.wasMoved = false;

		this.image = new Image();
		this.image.src = GUI.StrObject.path + this.name + '/' + this.data.texture;

		this.speechTimeout = null;
		this.speechText = null;

		this.nodeId = null;

		this.title = null;
		this.subTitle = null;

		this.showFlag = false;

		this.state = 0;

		this.setNodeId = function(id)
		{
			this.nodeId = id;
		}

		this.setMoveSpeed = function(ms)
		{
			this.ms = ms;
		}

		this.updateTexture = function(texture)
		{
			if(texture)
			{
				this.image = new Image();
				this.image.src = GUI.StrObject.path + this.name + '/' + texture;
			}
			else
			{
				this.image.src = this.image.src;
			}
		}

		this.setAnimation = function(name, speed)
		{
			if(!this.data.animations[name])
			{
				console.log('Animation "'+this.name+'" -> "'+name+'" not found.');
				return;
			}

			if(!speed)
			{
				speed = 1000;
				console.log('Speed unspecified for animation "'+this.name+'" -> "'+name+'", setting to '+speed);
			}

			this.animation = this.data.animations[name];
			this.currentFrame = 0;
			this.currentTime = 0;
			this.frameDuration = speed/this.animation.frameCount;
		}

		this.nextFrame = function()
		{
			// return frame
			var frame = this.animation.frames[this.currentFrame];

//			console.log(this.currentFrame);

			// increment counter
/*			this.currentFrame++;
			if(this.currentFrame == this.animation.frameCount)
			{
				this.currentFrame = 0;
			}*/

			// Prevent from calculating all the stuff if there's just a single frame
			if(this.animation.frameCount == 1)
			{
				return frame;
			}

			// Quantization by time
			this.currentFrame = Math.floor(this.currentTime / this.frameDuration);
			if(this.currentFrame >= this.animation.frameCount)
			{
				this.currentFrame = 0;
				this.currentTime = 0;
			}

			this.currentTime += GUI.frameDuration;

			return frame;
		}

		this._updatePos = function()
		{
			if(!this.moveStarted)
			{
				if(this.wasMoved)
				{
					this.x = this.tx;
					this.y = this.ty;
				}
				return;
			}

			var now = (new Date()).getTime();

			// How much time passed
			var dt = now - this.moveStarted;

			// Total move delta
			var dx = this.tx - this.x;
			var dy = this.ty - this.y;

			// Calculate signs for proper direction
			var sx = (dx > 0) ? 1 : -1;
			var sy = (dy > 0) ? 1 : -1;

			if(!dx && !dy)
			{
				this.onIdle();
				return;
			}

			// Distance
			var dist = Math.sqrt(dx*dx + dy*dy);

			// Points per millisecond
			var ppm = this.ms / 1000;

			// How much distance unit already passed
			var passed = ppm * dt;

//			console.log('coords: '+this.x+', '+this.y)

			if(passed > dist)
			{
				this.moveStarted = null;
				this.onIdle();
				return;
			}

			var k = passed / dist;

			this.x = this.x + dx * k;
			this.y = this.y + dy * k;

			this.moveStarted = (new Date()).getTime();
		}

		this.setPos = function(x, y)
		{
			this.x = x;
			this.y = y;
		}

		this.setTitle = function(title)
		{
			this.title = title;
		}

		this.setSubTitle = function(subTitle)
		{
			this.subTitle = subTitle;
		}

		this.getX = function()
		{
			return this.x;
		}

		this.getY = function()
		{
			return this.x;
		}

		this.getPos = function()
		{
			return {
				x: this.x,
				y: this.y
			};
		}

		this.draw = function(canvas, camera)
		{
			this._updatePos();

			// I'm really gonna fix this and make cursor a StrObject -_-
			if(this.showFlag && (this.state == GUI.StrObject.MOVING))
			{
				canvas.drawImage(GUI.storage.getImage('flag.png'), 0, 0, 32, 32, this.tx - camera.getX()-6, this.ty - camera.getY() - 32);
			}

			// Draw frame
			var frame = this.nextFrame();
			canvas.drawImage(this.image, frame[0], frame[1], this.data.width, this.data.height, ~~(this.x-this.data.width/2 - camera.getX()), ~~(this.y-this.data.height - camera.getY()));

			if(this.title)
			{
				canvas.fillText('#333', this.x - camera.getX(), this.y - this.data.height - 12 - camera.getY(), '12px sans-serif', this.title, true);
				canvas.fillText('#FFF', this.x - camera.getX() - 1, this.y - this.data.height - 12 - camera.getY() - 1, '12px sans-serif', this.title, true);
			}

			if(this.subTitle)
			{
				canvas.fillText('#333', this.x - camera.getX(), this.y - this.data.height - camera.getY(), '10px sans-serif', this.subTitle, true);
				canvas.fillText('#FFF', this.x - camera.getX() - 1, this.y - this.data.height - camera.getY() - 1, '10px sans-serif', this.subTitle, true);
			}

			// Draw speech bubble
			if(this.speechTimeout)
			{
//				var size = canvas.measureText(this.speechText);

				var size = {width: this.speechText.length*7, height: 10};

				var cx = this.x - camera.getX();
				var cy = this.y - camera.getY() - this.data.height - 45;

//				canvas.fillRect('rgba(255, 255, 255, 0.7)', this.x - size.width/2 - camera.getX(), this.y - this.data.height - size.height - 40 - camera.getY(), size.width + 10, size.height + 10, false);
//				canvas.fillRect('rgba(255, 255, 255, 0.7)', this.x - camera.getX() - size.width-size.height/2, this.y - camera.getY() - this.data.height - 45 - size.height*3, size.width+size.height, size.height*3, true); //this.y - this.data.height - size.height - 40 - camera.getY(), size.width + 10, size.height + 10, false);
				canvas.fillRect('rgba(0, 0, 0, 0.7)', cx, cy, size.width+size.height, size.height*2, true);//, '#000', 1); //this.y - this.data.height - size.height - 40 - camera.getY(), size.width + 10, size.height + 10, false);
				canvas.drawImage(GUI.storage.getImage('speech-link.png'), 0, 0, 15, 6, cx-7, cy+size.height, true);
				canvas.fillText('#FFF', cx, cy+4, '12px Monospace', this.speechText, true);
/*				alert(JSON.stringify({
					fillStyle: '#FFFFFF',
					x: this.x - this.data.width*2, y: this.y - this.data.height*3,
					width: this.data.width*5, height: this.data.height,
					fromCenter: false
				}));*/
			}

		}

		this.getDir = function(dx, dy)
		{
			// What the hell!
			if(dy >= Math.abs(dx))
			{
				return 2;
			}
			else if(dx >= Math.abs(dy))
			{
				return 1;
			}
			else if(dy <= -Math.abs(dx))
			{
				return 0;
			}
			else
			{
				return 3;
			}
		}

		this.getDir8 = function(dx, dy)
		{
			// TODO
		}

		this.moveTo = function(x, y, showFlag)
		{
//			this.x = x;
//			this.y = y;
//			return;

			this.tx = x;
			this.ty = y;

			this.showFlag = showFlag;

//			this.ms = speed;
			this.moveStarted = (new Date()).getTime();
			this.wasMoved = true;

			var dx = this.tx - this.x;
			var dy = this.ty - this.y;

			var dir = this.getDir(dx, dy);

			this.onMove(dir);
		}

		this.setLookDir = function(dir)
		{
			if(this.state != GUI.StrObject.MOVING)
			{
				this.setAnimation('stand-'+dir, 1000);
			}
		}

/*		this.onMove = function()
		{
			console.log(this.name+'->onMove not registered');
		}

		this.onIdle = function()
		{
			console.log(this.name+'->onIdle not registered');
		}*/

		this.onMove = function(dir)
		{
			this.state = GUI.StrObject.MOVING;
			this.setAnimation('walk-'+dir, 600);
		}
		this.onIdle = function()
		{
			this.state = GUI.StrObject.IDLE;
			this.setAnimation('default', 600);
		}

		this.attachSpeech = function(text, duration)
		{
			if(this.speechTimeout)
			{
				window.clearInterval(this.speechTimeout);
			}
			this.speechText = text;
			var obj = this;
			this.speechTimeout = window.setTimeout(function(){
				obj.speechTimeout = null;
			}, duration);
		}
	},

	StrMap: function(name, data)
	{
		this.name = name;
		this.data = data;

		this.texture = this.data.texture;
		this.textureFull = GUI.StrMap.path + this.name + '/' + this.texture;
		this.image = new Image();
		this.image.src = this.textureFull;
		this.textureSize = this.data.textureSize;

		var names = [];
		for(key in this.data.objects)
		{
			names.push(this.data.objects[key].name);
		}

//		GUI.engine.map = this;

//		alert(names);

		var obj = this;

//		GUI.storage.precacheObjects({objects:names}, function(){
//			client.send({a:'loaded'})
//			for(key in obj.data.objects)
//			{
//				var object = GUI.storage.getObject(obj.data.objects[key].name);
//				object.setPos(obj.data.objects[key].x, obj.data.objects[key].y);
//				GUI.engine.addObject(object);
//			}
//		});

		this.draw = function(canvas, camera)
		{
			patt = canvas.createPattern(this.textureFull, "repeat");

			var cam = camera.getPos();

//			var tx = camera.getX() % this.textureSize;
//			var ty = camera.getY() % this.textureSize;
			var tx = cam.x % this.textureSize;
			var ty = cam.y % this.textureSize;

			canvas.translateCanvas(-tx, -ty);
			canvas.fillRect(patt, -this.textureSize, -this.textureSize, canvas.width()+this.textureSize*2, canvas.height()+this.textureSize*2, false);
			canvas.translateCanvas(tx, ty);
		}
	},

	Camera: function(canvas)
	{
		this.canvas = canvas;
		this.context = canvas.get2d();

		this.canvasWidthHalf = this.canvas.width()/2;
		this.canvasHeightHalf = this.canvas.height()/2;

		this.attachTo = function(target)
		{
			this.target = target;
		}

		this.updatePos = function()
		{
			this.cachedX = this.target.x - this.canvasWidthHalf;
			this.cachedY = this.target.y - this.canvasHeightHalf;
		}

		this.getPos = function()
		{
			return {
				x: this.cachedX,
				y: this.cachedY
			};
		}

		this.getX = function()
		{
//			return this.target.x - this.canvasWidthHalf;
			return this.cachedX;
		}

		this.getY = function()
		{
//			return this.target.y - this.canvasHeightHalf;
			return this.cachedY;
		}

		this.translateObjectCoords = function(object)
		{
			return {
				x: object.x - this.canvasWidthHalf,
				y: target.y - this.canvasHeightHalf
			};
		}
	},

	Particles: function(x, y, radius, size, color)
	{
		this.count = 5;
		this.seeds = [];
		this.x = x;
		this.y = y;
		this.radius = radius;
		this.size = size;
		this.color = color;
//		this.start = (new Date()).getTime();

		for(i=0; i<this.count; i++)
		{
			this.seeds[i] = Math.random() * 50000;
		}

		this.draw = function(canvas, camera)
		{
			var time = (new Date()).getTime();
			for(i=0; i<this.count; i++)
			{
				var seed = time + this.seeds[i];
//				var color = ~~(Math.random() * 899) + 100;
				canvas.fillArc(this.color, this.x + Math.sin(seed/100 * 1.1)*radius - camera.getX(), this.y + Math.cos(seed/100 * 0.8)*radius - camera.getY(), this.size);
			}
		}
	},

	LoadingScreen: function()
	{
		this.draw = function(canvas)
		{
			canvas.fillRect(
				'#111',
				0, 0,
				canvas.width(), canvas.height(),
				false
			);

			canvas.fillText(
				'#55AAEE',
				canvas.width() / 2, canvas.height() / 1.5,
				'36px sans-serif',
				'Loading',
				true
			);
		}
	},

	ShakeEffect: function(velocity)
	{
		this.velocity = velocity;

		this.draw = function(canvas)
		{
			var time = (new Date()).getTime();
			canvas.translateCanvas(
				Math.sin(time/10)*this.velocity,
				Math.cos(time/10 * 0.8)*this.velocity
			);
		}
	},

	HUD: function()
	{
		this.overlay = null;//GUI.storage.getImage('hud.png');

		this.angle = 0;

		this.skills = {
			0:{ready:true,started:null,cooldown:null,icon:GUI.storage.getImage('skills/tornado.png')},
			1:{ready:true,started:null,cooldown:null,icon:null},
			2:{ready:true,started:null,cooldown:null,icon:null},
			3:{ready:true,started:null,cooldown:null,icon:null},
			4:{ready:true,started:null,cooldown:null,icon:null},
			5:{ready:true,started:null,cooldown:null,icon:null},
			6:{ready:true,started:null,cooldown:null,icon:null},
			7:{ready:true,started:null,cooldown:null,icon:null},
			8:{ready:true,started:null,cooldown:null,icon:null},
			9:{ready:true,started:null,cooldown:null,icon:null}
		};

		this._skillReady = function(skill)
		{
			return this.skills[skill].ready;
		}

		this.startCooldown = function(skill, cooldown)
		{
			if(!this._skillReady(skill))
			{
				return false;
			}

			this.skills[skill].ready = false;
			this.skills[skill].started = (new Date()).getTime();
			this.skills[skill].cooldown = cooldown;

			return true;
		}

/*		this.cooldown = function(skill)
		{
			if
		}*/

		this.draw = function(canvas)
		{
			if(!this.overlay)
			{
				this.overlay = GUI.storage.getImage('hud.png');
			}

//			canvas.fillArc('rgba(0, 0, 0, 0.5)', 100, 100, 50, 0, Math.PI/2);
//			canvas.fillArc('#F00', 0, 0, 100);

//			var ctx = canvas.get2d();
//			ctx.beginPath();
//			ctx.rect(180, 180, 40, 40);
//			ctx.clip();

//			canvas.fillArc('rgba(0, 0, 0, 0.5)', 200, 200, 30, this.angle - Math.PI/2, Math.PI*1.5);

			var now = (new Date()).getTime();

			for(i=0; i<10; i++)
			{
				if(this.skills[i].icon)
				{
					canvas.drawImage(this.skills[i].icon, 0, 0, 48, 48, 130+i*60-24, 450-24);
				}
				if(!this.skills[i].ready)
				{
					var lasted = now - this.skills[i].started;
					var left = (~~((this.skills[i].cooldown - lasted)/1000)+1)+' s';
					var angle = (lasted / this.skills[i].cooldown) * Math.PI*2;
					if(angle >= Math.PI*2)
					{
						this.skills[i].ready = true;
					}
					else
					{
						canvas.fillSquareArc('rgba(0, 0, 0, 0.7)', 130+i*60, 450, 24, angle - Math.PI/2, Math.PI*1.5);
						canvas.fillText('#000', 130+i*60, 450+1, '12px sans-serif', left, true);
						canvas.fillText('#FFF', 130+i*60, 450, '12px sans-serif bold', left, true);
					}
//					canvas.fillSquareArc('rgba(0, 0, 0, 0.7)', 190, 450, 24, angle - Math.PI/2, Math.PI*1.5);
				}
			}

			canvas.drawImage(this.overlay, 0, 0, 800, 480, 0, 0);

//			this.angle += 0.05;
		}
	},

	Window: function()
	{
		this.title = 'Untitled window';
		this.alive = true;

		this.setTitle = function(title)
		{
			this.title = title;
			return this;
		}

		this.testCursor = function(x, y)
		{
			// OMG :(
			return ((x>this.left) && (x<this.left+this.width) && (y>this.top) && (y<this.top+this.height)) ?
			((y<this.top+20) ? ((x > this.left+this.width-20) ? GUI.Window.HAS_CLOSE : GUI.Window.HAS_TITLEBAR) : GUI.Window.HAS_BODY) : GUI.Window.HAS_NONE;
		}

		this.setGeometry = function(left, top, width, height)
		{
			this.left = left;
			this.top = top;
			this.width = width;
			this.height = height;
		}

		this.draw = function(canvas)
		{
			canvas.fillRect('rgba(0, 0, 0, 0.7)', this.left, this.top, this.width, this.height, false, 'rgba(0, 0, 32, 0.8)', 2);
			canvas.fillRect('rgba(0, 128, 256, 0.4)', this.left, this.top, this.width, 20, false, 'rgba(0, 0, 32, 0.8)', 2);
			canvas.fillRect('rgba(168, 0, 0, 1)', this.left+this.width-20, this.top, 20, 20, false, 'rgba(0, 0, 32, 0.8)', 2);
			canvas.fillText('#FFF', this.left + this.width/2, this.top + 14, '12px sans-serif', this.title, true);
		}

		this.drag = function(dx, dy)
		{
			this.left += dx;
			this.top += dy;
		}

		this.close = function()
		{
			this.alive = false;
		}
	},

	InventoryWindow: function()
	{
		this.prototype = new Window();
	}
};

window.addEventListener('load', function(){
});
